/*******************************************************************************
 * Copyright (c) 2004, 2008 Actuate Corporation.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * https://www.eclipse.org/legal/epl-2.0/.
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 *
 * Contributors:
 *  Actuate Corporation  - initial API and implementation
 *******************************************************************************/

package org.eclipse.birt.report.engine.api.impl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.eclipse.birt.core.archive.IDocArchiveReader;
import org.eclipse.birt.core.archive.RAInputStream;
import org.eclipse.birt.core.script.ParameterAttribute;
import org.eclipse.birt.core.util.IOUtil;
import org.eclipse.birt.report.engine.api.EngineException;
import org.eclipse.birt.report.engine.api.IBookmarkInfo;
import org.eclipse.birt.report.engine.api.IReportDocumentHelper;
import org.eclipse.birt.report.engine.api.IReportRunnable;
import org.eclipse.birt.report.engine.api.ITOCTree;
import org.eclipse.birt.report.engine.api.InstanceID;
import org.eclipse.birt.report.engine.api.TOCNode;
import org.eclipse.birt.report.engine.api.impl.LinkedObjectManager.LinkedEntry;
import org.eclipse.birt.report.engine.content.IContent;
import org.eclipse.birt.report.engine.content.impl.BookmarkContent;
import org.eclipse.birt.report.engine.content.impl.ReportContent;
import org.eclipse.birt.report.engine.executor.ApplicationClassLoader;
import org.eclipse.birt.report.engine.executor.ExecutorManager;
import org.eclipse.birt.report.engine.extension.engine.IReportDocumentExtension;
import org.eclipse.birt.report.engine.extension.engine.IReportEngineExtension;
import org.eclipse.birt.report.engine.i18n.MessageConstants;
import org.eclipse.birt.report.engine.internal.document.DocumentExtension;
import org.eclipse.birt.report.engine.internal.document.IPageHintReader;
import org.eclipse.birt.report.engine.internal.document.PageHintReader;
import org.eclipse.birt.report.engine.internal.document.v3.ReportContentReaderV3;
import org.eclipse.birt.report.engine.internal.document.v4.InstanceIDComparator;
import org.eclipse.birt.report.engine.internal.executor.doc.Fragment;
import org.eclipse.birt.report.engine.internal.index.DocumentIndexReader;
import org.eclipse.birt.report.engine.internal.index.IDocumentIndexReader;
import org.eclipse.birt.report.engine.ir.EngineIRReader;
import org.eclipse.birt.report.engine.ir.Report;
import org.eclipse.birt.report.engine.presentation.IPageHint;
import org.eclipse.birt.report.engine.presentation.PageSection;
import org.eclipse.birt.report.engine.toc.ITOCReader;
import org.eclipse.birt.report.engine.toc.ITreeNode;
import org.eclipse.birt.report.engine.toc.TOCBuilder;
import org.eclipse.birt.report.engine.toc.TOCReader;
import org.eclipse.birt.report.engine.toc.TOCView;
import org.eclipse.birt.report.engine.toc.TreeNode;
import org.eclipse.birt.report.engine.toc.document.TOCReaderV0;
import org.eclipse.birt.report.model.api.DesignElementHandle;
import org.eclipse.birt.report.model.api.ModuleOption;
import org.eclipse.birt.report.model.api.ModuleUtil;
import org.eclipse.birt.report.model.api.ReportDesignHandle;
import org.eclipse.birt.report.model.api.ReportElementHandle;
import org.eclipse.birt.report.model.api.ReportItemHandle;

import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.ULocale;

/**
 * core stream format TAG DOCUMENT VERSION
 *
 * TAG CORE_VERSION_0 DOCUMENT_VERSION TAG
 *
 * CORE_VERSION_1 DOCUMENT_VERSION MAP
 */
public class ReportDocumentReader implements IReportletDocument, ReportDocumentConstants, IReportDocumentHelper {

	static private Logger logger = Logger.getLogger(ReportDocumentReader.class.getName());

	private ReportEngine engine;
	private IDocArchiveReader archive;
	private Map moduleOptions;
	/*
	 * version, parameters, globalVariables are loaded from core stream.
	 */
	private String coreVersion;
	private Map properties = new HashMap();
	private HashMap parameters;
	private HashMap globalVariables;
	/**
	 * used to load the page hints
	 */
	private IPageHintReader pageHintReader;
	private ITOCReader tocReader;
	private IDocumentIndexReader indexReader;
	/** Design name */
	private String systemId;

	private int checkpoint = CHECKPOINT_INIT;

	private long pageCount;

	private boolean sharedArchive;

	private ApplicationClassLoader applicationClassLoader;

	private ReportRunnable preparedRunnable = null;

	private ReportRunnable reportRunnable = null;

	private boolean coreStreamLoaded = false;

	private LinkedEntry<ReportDocumentReader> engineCacheEntry;

	private byte[] bodyData;
	private TreeNode cachedTreeV0;

	public ReportDocumentReader(ReportEngine engine, IDocArchiveReader archive, boolean sharedArchive)
			throws EngineException {
		this(null, engine, archive, sharedArchive);
	}

	public ReportDocumentReader(ReportEngine engine, IDocArchiveReader archive) throws EngineException {
		this(null, engine, archive, false);
	}

	public ReportDocumentReader(String systemId, ReportEngine engine, IDocArchiveReader archive, Map options)
			throws EngineException {
		this(systemId, engine, archive, false, options);
	}

	public ReportDocumentReader(String systemId, ReportEngine engine, IDocArchiveReader archive, boolean sharedArchive)
			throws EngineException {
		this(systemId, engine, archive, sharedArchive, null);
	}

	public ReportDocumentReader(String systemId, ReportEngine engine, IDocArchiveReader archive, boolean sharedArchive,
			Map options) throws EngineException {
		this.engine = engine;
		this.archive = archive;
		this.systemId = systemId;
		this.sharedArchive = sharedArchive;
		this.moduleOptions = new HashMap();
		this.moduleOptions.put(ModuleOption.PARSER_SEMANTIC_CHECK_KEY, Boolean.FALSE);
		if (options != null) {
			this.moduleOptions.putAll(options);
		}

		loadCoreStreamHeader();
	}

	@Override
	public IDocArchiveReader getArchive() {
		return this.archive;
	}

	@Override
	public String getVersion() {
		return (String) properties.get(BIRT_ENGINE_VERSION_KEY);
	}

	@Override
	public String getProperty(String key) {
		return (String) properties.get(key);
	}

	static public class ReportDocumentCoreInfo {

		public int checkpoint;
		public long pageCount;
		public String systemId;
		public HashMap globalVariables;
		public HashMap parameters;
		public ITOCReader tocReader;
		public IDocumentIndexReader indexReader;
	}

	private void loadCoreStreamHeader() throws EngineException {
		try {
			Object lock = archive.lock(CORE_STREAM);
			try {
				synchronized (lock) {
					RAInputStream in = archive.getStream(CORE_STREAM);
					try (in) {
						DataInputStream di = new DataInputStream(in);

						// check the document version and core stream version
						checkVersion(di);

						// load the stream header, check point, system id and
						// page count
						loadCoreStreamHeader(di);
					}
				}
			} finally {
				archive.unlock(lock);
			}
		} catch (IOException ee) {
			close();
			throw new EngineException(MessageConstants.REPORT_DOCUMENT_OPEN_ERROR, ee);
		}
	}

	private void loadCoreStreamHeader(DataInputStream di) throws IOException {
		// load info into a document info object
		ReportDocumentCoreInfo documentInfo = new ReportDocumentCoreInfo();

		loadCoreStreamHeader(di, documentInfo);

		if (checkpoint != documentInfo.checkpoint) {
			checkpoint = documentInfo.checkpoint;
			pageCount = documentInfo.pageCount;
			if (systemId == null) {
				systemId = documentInfo.systemId;
			}
		}
	}

	private void loadCoreStreamHeader(DataInputStream di, ReportDocumentCoreInfo documentInfo) throws IOException {
		if (CORE_VERSION_UNKNOWN.equals(coreVersion)) {
			loadCoreStreamHeaderUnknown(di, documentInfo);
		} else if (CORE_VERSION_0.equals(coreVersion) || CORE_VERSION_1.equals(coreVersion)
				|| CORE_VERSION_2.equals(coreVersion)) {
			loadCoreStreamHeaderV0(di, documentInfo);
		} else {
			throw new IOException(MessageConstants.UNSUPPORTED_CORE_STREAM_VERSION + coreVersion);
		}
	}

	/**
	 * read the core stream header which has version UNKNOWN and V0.
	 *
	 * The core stream header contains 3 field:
	 * <li>check point</li>
	 * <li>page count</li>
	 * <li>system id</li>
	 *
	 * @param coreStream   the core stream. The version has been loaded from the
	 *                     stream.
	 * @param documentInfo the loaded information is saved into this object.
	 * @throws IOException
	 */
	private void loadCoreStreamHeaderUnknown(DataInputStream coreStream, ReportDocumentCoreInfo documentInfo)
			throws IOException {
		documentInfo.checkpoint = CHECKPOINT_INIT;
		documentInfo.pageCount = PAGECOUNT_INIT;
		if (!archive.exists(CHECKPOINT_STREAM)) {
			// no check point stream, old version, return -1
			documentInfo.checkpoint = CHECKPOINT_END;
			initializePageHintReader();
			if (pageHintReader != null) {
				documentInfo.pageCount = pageHintReader.getTotalPage();
			}
		} else {
			RAInputStream in = archive.getStream(CHECKPOINT_STREAM);
			try (in) {
				DataInputStream di = new DataInputStream(in);
				documentInfo.checkpoint = IOUtil.readInt(di);
				documentInfo.pageCount = IOUtil.readLong(di);
			}
		}
		// load the report design name
		documentInfo.systemId = IOUtil.readString(coreStream);
	}

	/**
	 * load the core stream header for stream with version V1 and V2
	 *
	 * the core stream header contains 3 field:
	 *
	 * <li>check point</li>
	 * <li>page count</li>
	 * <li>system id</li>
	 *
	 * @param di           core stream, the version has been loaded from this
	 *                     stream.
	 * @param documentInfo the header is saved into this object.
	 * @throws IOException
	 */
	private void loadCoreStreamHeaderV0(DataInputStream di, ReportDocumentCoreInfo documentInfo) throws IOException {
		documentInfo.checkpoint = CHECKPOINT_INIT;
		documentInfo.pageCount = PAGECOUNT_INIT;

		documentInfo.checkpoint = IOUtil.readInt(di);
		documentInfo.pageCount = IOUtil.readLong(di);

		// load the report design name
		documentInfo.systemId = IOUtil.readString(di);
	}

	private void loadCoreStreamLazily() {
		if (!coreStreamLoaded) {
			synchronized (this) {
				if (!coreStreamLoaded) {
					try {
						loadCoreStream();
					} catch (IOException ee) {
						logger.log(Level.SEVERE, "Failed to load core stream", ee); //$NON-NLS-1$
					}
				}
			}
		}
	}

	@Override
	synchronized public void refresh() {
		try {
			loadCoreStream();
		} catch (IOException ee) {
			logger.log(Level.SEVERE, "Failed to refresh", ee); //$NON-NLS-1$
		}
	}

	protected void loadCoreStream() throws IOException {
		Object lock = archive.lock(CORE_STREAM);
		try {
			synchronized (lock) {
				RAInputStream in = archive.getStream(CORE_STREAM);
				try (in) {
					DataInputStream di = new DataInputStream(in);

					// check the document version and core stream version
					checkVersion(di);

					loadCoreStream(di);

					coreStreamLoaded = true;
				}
			}
		} finally {
			archive.unlock(lock);
		}
	}

	private void loadCoreStream(DataInputStream di) throws IOException {
		// load info into a document info object
		ReportDocumentCoreInfo documentInfo = new ReportDocumentCoreInfo();

		loadCoreStreamHeader(di, documentInfo);
		loadCoreStreamBody(di, documentInfo);

		// save the document info into the object.
		checkpoint = documentInfo.checkpoint;
		pageCount = documentInfo.pageCount;
		globalVariables = documentInfo.globalVariables;
		parameters = documentInfo.parameters;
		if (checkpoint == CHECKPOINT_END) {
			if (indexReader == null) {
				indexReader = documentInfo.indexReader;
			} else if (documentInfo.indexReader != null) {
				documentInfo.indexReader.close();
			}
			if (tocReader == null) {
				tocReader = documentInfo.tocReader;
				if (tocReader != null) {
					cachedTreeV0 = (TreeNode) tocReader.readTree();
				}
			} else if (documentInfo.tocReader != null) {
				documentInfo.tocReader.close();
			}
		}
	}

	private void loadCoreStreamBody(DataInputStream di, ReportDocumentCoreInfo documentInfo) throws IOException {
		if (CORE_VERSION_UNKNOWN.equals(coreVersion)) {
			loadCoreStreamBodyUnknown(di, documentInfo);
		} else if (CORE_VERSION_0.equals(coreVersion) || CORE_VERSION_1.equals(coreVersion)) {
			loadCoreStreamBodyV0(di, documentInfo);
		} else if (CORE_VERSION_2.equals(coreVersion)) {
			loadCoreStreamBodyV2(di, documentInfo);
		} else {
			throw new IOException("unsupported core stream version: " + coreVersion);
		}
	}

	protected void loadCoreStreamBodyUnknown(DataInputStream coreStream, ReportDocumentCoreInfo documentInfo)
			throws IOException {
		// load the report parameters
		loadCoreStreamBodyToBuffer(coreStream);
		if (documentInfo.checkpoint == CHECKPOINT_END) {
			documentInfo.indexReader = new DocumentIndexReader(IDocumentIndexReader.VERSION_0, archive);
		}
	}

	protected void loadCoreStreamBodyV0(DataInputStream coreStream, ReportDocumentCoreInfo documentInfo)
			throws IOException {
		Map originalParameters = IOUtil.readMap(coreStream);
		documentInfo.parameters = convertToCompatibleParameter(originalParameters);
		// load the persistence object
		documentInfo.globalVariables = (HashMap) IOUtil.readMap(coreStream);
		if (documentInfo.checkpoint == CHECKPOINT_END) {
			HashMap<String, Long> bookmarks = readMap(coreStream);
			documentInfo.tocReader = new TOCReader(coreStream, null);
			HashMap<String, Long> reportletsIndexById = readMap(coreStream);
			HashMap<String, Long> reportletsIndexByBookmark = readMap(coreStream);
			documentInfo.indexReader = new DocumentIndexReader(IDocumentIndexReader.VERSION_1,
					reportletsIndexByBookmark, reportletsIndexById, bookmarks);
		}
	}

	protected void loadCoreStreamBodyV2(DataInputStream coreStream, ReportDocumentCoreInfo documentInfo)
			throws IOException {
		loadCoreStreamBodyToBuffer(coreStream);
		if (documentInfo.checkpoint == CHECKPOINT_END) {
			documentInfo.indexReader = new DocumentIndexReader(IDocumentIndexReader.VERSION_2, archive);
		}
	}

	private void loadCoreStreamBodyToBuffer(DataInputStream stream) throws IOException {
		byte[] datas = new byte[1024];
		int length = -1;
		ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
		while ((length = stream.read(datas)) > 0) {
			byteOut.write(datas, 0, length);
		}
		bodyData = byteOut.toByteArray();
	}

	private HashMap<String, Long> readMap(DataInputStream di) throws IOException {
		HashMap<String, Long> map = new HashMap<>();
		long count = IOUtil.readLong(di);
		for (long i = 0; i < count; i++) {
			String bookmark = IOUtil.readString(di);
			long pageNumber = IOUtil.readLong(di);
			map.put(bookmark, Long.valueOf(pageNumber));
		}
		return map;
	}

	private HashMap convertToCompatibleParameter(Map parameters) {
		if (parameters == null) {
			return null;
		}
		HashMap result = new HashMap();
		Iterator iterator = parameters.entrySet().iterator();
		while (iterator.hasNext()) {
			Map.Entry entry = (Map.Entry) iterator.next();
			Object key = entry.getKey();
			Object valueObj = entry.getValue();
			ParameterAttribute paramAttr = null;
			if (valueObj instanceof ParameterAttribute) {
				paramAttr = (ParameterAttribute) valueObj;
			} else if (valueObj instanceof Object[]) {
				Object[] values = (Object[]) valueObj;
				if (values.length == 2) {
					Object value = values[0];
					if (values[1] == null || values[1] instanceof String) {
						paramAttr = new ParameterAttribute(value, (String) values[1]);
					} else if (values[1] instanceof String[]) {
						paramAttr = new ParameterAttribute((Object[]) value, (String[]) values[1]);
					}

				}
			}
			if (paramAttr == null) {
				paramAttr = new ParameterAttribute(valueObj, null);
			}
			result.put(key, paramAttr);
		}
		return result;
	}

	/**
	 * <pre>
	 *    		TAG		CORE_V	DOC_V	ENG_V	HIGHLIGHT	VERSION
	 * 2.0.0	x		NULL	1.0.0	NULL	FALSE		2.0.0
	 * 2.0.1	x		NULL	1.0.0	NULL	FALSE		2.0.0
	 * 2.0.2	x		NULL	1.0.0	NULL	FALSE		2.0.0
	 * 2.1.0	x		NULL	2.1.0	NULL	TRUE		2.1.0
	 * 2.1.1	x		NULL	2.1.0	NULL	TRUE		2.1.0
	 * 2.1.2	x		NULL	2.1.0	NULL	TRUE		2.1.0
	 * 2.1.3	x		v0		2.1.3	NULL	TRUE		2.1.3
	 * 2.2.0	x		v0		2.1.3	NULL	TRUE		2.1.3
	 * 2.2.1	x		v1		2.1.3	2.2.1	FALSE		2.2.1
	 * 2.2.2	x		v1		2.1.3	2.2.1	FALSE		2.2.1
	 * 2.3.0	x		v1		2.1.3	2.2.1	FALSE		2.2.1
	 * 2.3.1	x		v1		2.1.3	2.2.1	FALSE		2.2.1
	 * 2.3.2	x		v1		2.1.3	2.3.2	FALSE		2.3.2
	 * 2.5.0	x		v1		2.1.3	2.5.0	FALSE		2.5.0
	 * </pre>
	 */
	protected void checkVersion(DataInputStream di) throws IOException {
		String tag = IOUtil.readString(di);
		if (!REPORT_DOCUMENT_TAG.equals(tag)) {
			throw new IOException("unknown report document tag" + tag);//$NON-NLS-1$
		}
		String docVersion = IOUtil.readString(di);
		if (docVersion == null) {
			throw new IOException("invalid core stream format");//$NON-NLS-1$
		}

		if (!docVersion.startsWith(CORE_VERSION_PREFIX)) {
			// there is no core version, so set the core version to unknown.
			coreVersion = CORE_VERSION_UNKNOWN;
		} else if (CORE_VERSION_0.equals(docVersion) || CORE_VERSION_1.equals(docVersion)
				|| CORE_VERSION_2.equals(docVersion)) {
			coreVersion = docVersion;
			docVersion = IOUtil.readString(di);
			if (CORE_VERSION_1.equals(coreVersion) || CORE_VERSION_2.equals(coreVersion)) {
				properties = IOUtil.readMap(di);
			}
		} else {
			throw new IOException("unknown core stream version" + tag);//$NON-NLS-1$
		}

		String[] supportedVersions = { REPORT_DOCUMENT_VERSION_1_0_0, REPORT_DOCUMENT_VERSION_2_1_0,
				REPORT_DOCUMENT_VERSION_2_1_3 };
		boolean supportedVersion = false;
		for (int i = 0; i < supportedVersions.length; i++) {
			if (supportedVersions[i].equals(docVersion)) {
				supportedVersion = true;
				break;
			}
		}
		if (!supportedVersion) {
			throw new IOException("unsupport report document version " + docVersion); //$NON-NLS-1$
		}

		// test if request extension are present
		String extensions = (String) properties.get(BIRT_ENGINE_EXTENSIONS);
		if (extensions != null && extensions.length() > 0) {
			String[] extIds = extensions.split(";");
			for (String extId : extIds) {
				IReportEngineExtension ext = engine.getEngineExtension(extId);
				if (ext == null) {
					throw new IOException("unsupported report extension:" + extId);
				}
			}
		}

		if (properties.get(BIRT_ENGINE_VERSION_KEY) == null) {
			if (REPORT_DOCUMENT_VERSION_1_0_0.equals(docVersion)) {
				properties.put(BIRT_ENGINE_VERSION_KEY, BIRT_ENGINE_VERSION_2_0_0);
			} else if (REPORT_DOCUMENT_VERSION_2_1_0.equals(docVersion)) {
				properties.put(BIRT_ENGINE_VERSION_KEY, BIRT_ENGINE_VERSION_2_1_0);
			} else if (REPORT_DOCUMENT_VERSION_2_1_3.equals(docVersion)) {
				properties.put(BIRT_ENGINE_VERSION_KEY, BIRT_ENGINE_VERSION_2_1_3);
			}
		}

		String version = getVersion();
		// FIXME: test if the version is later than BIRT_ENGINE_VERSION

		if (properties.get(DATA_EXTRACTION_TASK_VERSION_KEY) == null) {
			// check the data extraction task version
			if (BIRT_ENGINE_VERSION_2_0_0.equals(docVersion) || BIRT_ENGINE_VERSION_2_1_0.equals(version)) {
				properties.put(DATA_EXTRACTION_TASK_VERSION_KEY, DATA_EXTRACTION_TASK_VERSION_0);
			} else {
				properties.put(DATA_EXTRACTION_TASK_VERSION_KEY, DATA_EXTRACTION_TASK_VERSION_1);
			}
		}
		// assign the page-hint version
		if (properties.get(PAGE_HINT_VERSION_KEY) == null) {
			properties.put(PAGE_HINT_VERSION_KEY, PAGE_HINT_VERSION_2);
		}
	}

	@Override
	public void close() {
		if (tocReader != null) {
			try {
				tocReader.close();
			} catch (IOException ex) {
				logger.log(Level.SEVERE, "Failed to close the tocReader", ex); //$NON-NLS-1$
			}
			tocReader = null;
		}

		if (indexReader != null) {
			try {
				indexReader.close();
			} catch (IOException ex) {
				logger.log(Level.WARNING, "failed to close the index reader", ex);
			}
			indexReader = null;
		}

		if (pageHintReader != null) {
			pageHintReader.close();
			pageHintReader = null;
		}
		if (archive != null) {
			if (!sharedArchive) {
				try {
					archive.close();
				} catch (IOException ex) {
					logger.log(Level.SEVERE, "Failed to close the archive", ex); //$NON-NLS-1$
				}
			}
			archive = null;
		}

		if (extensions != null) {
			for (Map.Entry<String, IReportDocumentExtension> entry : extensions.entrySet()) {
				String name = entry.getKey();
				IReportDocumentExtension extension = entry.getValue();
				if (extension != null) {
					try {
						extension.close();
					} catch (EngineException ex) {
						logger.log(Level.SEVERE, "Failed to close the extension " + name, ex);
					}
				}
			}

			extensions.clear();
			extensions = null;
		}
		if (applicationClassLoader != null) {
			applicationClassLoader.close();
		}
		if (engineCacheEntry != null) {
			LinkedObjectManager<ReportDocumentReader> manager = engineCacheEntry.getManager();
			synchronized (manager) {
				manager.remove(engineCacheEntry);
			}
		}
		this.bodyData = null;
		this.engine = null;
		this.cachedTreeV0 = null;
		if (globalVariables != null) {
			globalVariables.clear();
			globalVariables = null;
		}
		if (moduleOptions != null) {
			moduleOptions.clear();
			moduleOptions = null;
		}
		this.preparedRunnable = null;
		this.reportRunnable = null;
		if (parameters != null) {
			parameters.clear();
			parameters = null;
		}
	}

	@Override
	public InputStream getDesignStream() {
		try {
			if (archive.exists(ORIGINAL_DESIGN_STREAM)) {
				return archive.getStream(ORIGINAL_DESIGN_STREAM);
			}
			return archive.getStream(DESIGN_STREAM);
		} catch (Exception ex) {
			logger.log(Level.SEVERE, "Failed to open the design!", ex); //$NON-NLS-1$
			return null;
		}
	}

	private ReportRunnable loadReportRunnable(String systemId, String streamName) {

		try {
			if (archive.exists(streamName)) {
				InputStream stream = archive.getStream(streamName);
				try (stream) {
					String name = systemId;
					if (name == null) {
						name = archive.getName();
					}
					ReportRunnable reportRunnable = (ReportRunnable) engine.openReportDesign(name, stream,
							moduleOptions);
					return reportRunnable;
				}
			}
		} catch (Exception ex) {
			logger.log(Level.SEVERE, "Failed to open the design!", ex); //$NON-NLS-1$
		}
		return null;
	}

	@Override
	public synchronized IReportRunnable getReportRunnable() {
		if (reportRunnable == null) {
			reportRunnable = loadReportRunnable(systemId, ORIGINAL_DESIGN_STREAM);
			if (reportRunnable != null) {
				reportRunnable.setPrepared(false);
			} else {
				reportRunnable = getOnPreparedRunnable();
			}
		}
		if (reportRunnable != null) {
			return reportRunnable.cloneRunnable();
		}
		return null;
	}

	@Override
	public synchronized IReportRunnable getPreparedRunnable() {
		ReportRunnable runnable = getOnPreparedRunnable();
		if (runnable != null) {
			return runnable.cloneRunnable();
		}
		return null;
	}

	@Override
	public synchronized IReportRunnable getDocumentRunnable() {
		return getOnPreparedRunnable();
	}

	@Override
	public ReportRunnable getOnPreparedRunnable() {
		if (preparedRunnable == null) {
			preparedRunnable = loadReportRunnable(systemId, DESIGN_STREAM);
			if (preparedRunnable != null) {
				preparedRunnable.setPrepared(true);
			}
		}
		return preparedRunnable;
	}

	/**
	 * @deprecated
	 */
	@Deprecated
	@Override
	public Map getParameterValues() {
		loadCoreStreamLazily();
		loadVariableLazily();
		Map result = new HashMap();
		if (parameters != null) {
			Iterator iterator = parameters.entrySet().iterator();
			while (iterator.hasNext()) {
				Map.Entry entry = (Map.Entry) iterator.next();
				String name = (String) entry.getKey();
				ParameterAttribute value = (ParameterAttribute) entry.getValue();
				result.put(name, value.getValue());
			}
		}
		return result;
	}

	public Map getParameterValues(ClassLoader loader) {
		Map result = new HashMap();
		try {
			Map parameters = loadParameters(loader);
			if (parameters != null) {
				Iterator iterator = parameters.entrySet().iterator();
				while (iterator.hasNext()) {
					Map.Entry entry = (Map.Entry) iterator.next();
					String name = (String) entry.getKey();
					ParameterAttribute value = (ParameterAttribute) entry.getValue();
					result.put(name, value.getValue());
				}
			}
		} catch (EngineException ex) {
			logger.log(Level.SEVERE, ex.getLocalizedMessage(), ex);
		}
		return result;
	}

	protected void loadVariableLazily() {
		if (globalVariables == null) {
			assert (bodyData != null);

			ClassLoader loader = getClassLoader();
			DataInputStream dataStream = new DataInputStream(new ByteArrayInputStream(bodyData));
			try {
				parameters = (HashMap) IOUtil.readMap(dataStream, loader);
				parameters = (HashMap) convertToCompatibleParameter(parameters);
				globalVariables = (HashMap) IOUtil.readMap(dataStream, loader);
			} catch (IOException ex) {
				logger.log(Level.SEVERE, "Failed to load parameters and variables in the core stream", ex); //$NON-NLS-1$
			}
		}
	}

	@Override
	public Map<String, ParameterAttribute> loadParameters(ClassLoader loader) throws EngineException {
		loadCoreStreamLazily();
		if (bodyData == null) {
			return parameters;
		}
		try {
			DataInputStream dataStream = new DataInputStream(new ByteArrayInputStream(bodyData));

			Map parameters = IOUtil.readMap(dataStream, loader);
			return convertToCompatibleParameter(parameters);
		} catch (IOException ee) {
			throw new EngineException("Failed to load parameters in the core stream", ee); //$NON-NLS-1$
		}
	}

	@Override
	public Map loadVariables(ClassLoader loader) throws EngineException {
		loadCoreStreamLazily();
		if (bodyData == null) {
			return globalVariables;
		}
		try {
			DataInputStream dataStream = new DataInputStream(new ByteArrayInputStream(bodyData));

			// skip the parameters
			IOUtil.readMap(dataStream, loader);
			// load the persistence object
			return IOUtil.readMap(dataStream, loader);
		} catch (IOException ee) {
			throw new EngineException("Failed to load variables in the core stream", ee); //$NON-NLS-1$
		}
	}

	@Override
	public Map getParameterDisplayTexts() {
		loadCoreStreamLazily();
		loadVariableLazily();
		Map result = new HashMap();
		if (parameters != null) {
			Iterator iterator = parameters.entrySet().iterator();
			while (iterator.hasNext()) {
				Map.Entry entry = (Map.Entry) iterator.next();
				String name = (String) entry.getKey();
				ParameterAttribute value = (ParameterAttribute) entry.getValue();
				result.put(name, value.getDisplayText());
			}
		}
		return result;
	}

	@Override
	public long getPageCount() {
		return pageCount;
	}

	public IPageHint getPageHint(long pageNumber) {
		initializePageHintReader();
		if (pageHintReader != null) {
			try {
				return pageHintReader.getPageHint(pageNumber);
			} catch (IOException ex) {
				logger.log(Level.WARNING, "Failed to load page hint " + pageNumber, ex);
			}
		}
		return null;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.birt.report.engine.api.IReportDocument#getPageNumber(java
	 * .lang.String)
	 */
	@Override
	public long getPageNumber(String bookmark) {
		if (!isComplete()) {
			return -1;
		}

		loadCoreStreamLazily();
		if (indexReader != null) {
			try {
				return indexReader.getPageOfBookmark(bookmark);
			} catch (IOException ex) {
				logger.log(Level.WARNING, "failed to load the page number of bookmark:" + bookmark, ex);
			}
		}
		return -1;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.birt.report.engine.api.IReportDocument#getBookmarks()
	 */
	@Override
	public List<String> getBookmarks() {
		if (!isComplete()) {
			return null;
		}

		loadCoreStreamLazily();
		if (indexReader != null) {
			try {
				return indexReader.getBookmarks();
			} catch (IOException ex) {
				logger.log(Level.WARNING, "failed to load the bookmark list", ex);
			}
		}
		return null;
	}

	public List<BookmarkContent> getBookmarkContents() {
		if (!isComplete()) {
			return null;
		}

		loadCoreStreamLazily();
		if (indexReader != null) {
			try {
				return indexReader.getBookmarkContents();
			} catch (IOException ex) {
				logger.log(Level.WARNING, "failed to load the bookmark info list", ex);
			}
		}
		return null;
	}

	@Override
	public List<IBookmarkInfo> getBookmarkInfos(Locale locale) throws EngineException {
		if (!isComplete()) {
			return null;
		}
		ArrayList<IBookmarkInfo> results = new ArrayList<>();

		loadCoreStreamLazily();
		if (indexReader != null) {
			try {
				List<BookmarkContent> bookmarks = indexReader.getBookmarkContents();
				if (bookmarks == null) {
					return null;
				}
				ReportDesignHandle report = this.getReportDesign();

				for (BookmarkContent bookmark : bookmarks) {
					String bookmarkString = bookmark.getBookmark();
					if (bookmarkString.startsWith(ExecutorManager.BOOKMARK_PREFIX)
							|| bookmarkString.indexOf(TOCBuilder.TOC_PREFIX) != -1) {
						continue;
					}
					long designId = bookmark.getElementId();
					DesignElementHandle handle = report.getElementByID(designId);
					if (handle == null) {
						continue;
					}
					String elementType = handle.getDefn().getName();
					String displayName = null;
					if (handle instanceof ReportItemHandle) {
						displayName = ((ReportItemHandle) handle).getBookmarkDisplayName();
					}
					if (locale != null) {
						if (handle instanceof ReportElementHandle) {
							ReportElementHandle elementHandle = (ReportElementHandle) handle;
							displayName = ModuleUtil.getExternalizedValue(elementHandle, bookmarkString, displayName,
									ULocale.forLocale(locale));

						}
					}
					results.add(new BookmarkInfo(bookmarkString, displayName, elementType));
				}
			} catch (IOException ex) {
				throw new EngineException(MessageConstants.BOOKMARK_FETCHING_EXCEPTION, ex);
			}
		}
		return results;
	}

	/**
	 * @param bookmark the bookmark that a page number is to be retrieved upon
	 * @return the page number that the bookmark appears
	 */
	public long getBookmark(String bookmark) {
		if (!isComplete()) {
			return -1;
		}

		loadCoreStreamLazily();
		if (indexReader != null) {
			try {
				return indexReader.getOffsetOfBookmark(bookmark);
			} catch (IOException ex) {
				logger.log(Level.WARNING, "failed to load the offset of bookmark:" + bookmark, ex);
			}
		}
		return -1;
	}

	/**
	 * @deprecated
	 */
	@Deprecated
	@Override
	public ITOCTree getTOCTree(String format, ULocale locale) {
		return getTOCTree(format, locale, TimeZone.getDefault());
	}

	/**
	 * @deprecated
	 */
	@Deprecated
	@Override
	public ITOCTree getTOCTree(String format, ULocale locale, TimeZone timeZone) {
		try {
			ITreeNode root = getTOCTree();
			if (root != null) {
				ReportDesignHandle report = ((ReportRunnable) getOnPreparedRunnable()).getReport();
				return new TOCView(root, report, locale, timeZone, format);
			}
		} catch (EngineException ex) {
			logger.log(Level.WARNING, ex.getMessage(), ex);
		}
		return null;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.birt.report.engine.api.IReportDocument#findTOC(java.lang.
	 * String)
	 */
	/**
	 * @deprecated
	 */
	@Deprecated
	@Override
	public TOCNode findTOC(String tocNodeId) {
		ITOCTree tree = getTOCTree("viewer", ULocale.getDefault());
		if (tree != null) {
			return tree.findTOC(tocNodeId);
		}
		return null;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.birt.report.engine.api.IReportDocument#findTOCByName(java
	 * .lang.String)
	 */
	/**
	 * @deprecated
	 */
	@Deprecated
	@Override
	public List findTOCByName(String tocName) {
		ITOCTree tree = getTOCTree("viewer", ULocale.getDefault());
		if (tree != null) {
			return tree.findTOCByValue(tocName);
		}
		return null;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.birt.report.engine.api.IReportDocument#getChildren(java.lang
	 * .String)
	 */
	/**
	 * @deprecated
	 */
	@Deprecated
	@Override
	public List getChildren(String tocNodeId) {
		TOCNode node = findTOC(tocNodeId);
		if (node != null) {
			return node.getChildren();
		}
		return null;
	}

	private void initializePageHintReader() {
		if (pageHintReader != null) {
			return;
		}
		synchronized (this) {
			if (pageHintReader != null) {
				return;
			}
			try {
				pageHintReader = new PageHintReader(this);
			} catch (IOException ex) {
				logger.log(Level.SEVERE, "can not open the page hint stream", ex);
			}
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.birt.report.engine.api.IReportDocument#getName()
	 */
	@Override
	public String getName() {
		return archive.getName();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.birt.report.engine.api.IReportDocument#getGlobalVariables()
	 */
	/**
	 * @deprecated
	 */
	@Deprecated
	@Override
	public Map getGlobalVariables(String option) {
		loadCoreStreamLazily();
		loadVariableLazily();
		return globalVariables;
	}

	@Override
	public long getPageNumber(InstanceID iid) {
		if (!isComplete()) {
			return -1;
		}
		initializePageHintReader();
		if (pageHintReader == null) {
			return -1;
		}

		int version = pageHintReader.getVersion();

		try {
			if (version == IPageHintReader.VERSION_0) {
				long offset = getInstanceOffset(iid);
				if (offset == -1) {
					return -1;
				}
				long totalPage = pageHintReader.getTotalPage();
				for (long pageNumber = 1; pageNumber <= totalPage; pageNumber++) {
					IPageHint hint = pageHintReader.getPageHint(pageNumber);
					PageSection section = hint.getSection(0);

					if (offset >= section.startOffset) {
						return pageNumber;
					}
				}
			}

			else if (version == IPageHintReader.VERSION_1) {
				long offset = getInstanceOffset(iid);
				if (offset == -1) {
					return -1;
				}
				long totalPage = pageHintReader.getTotalPage();
				for (long pageNumber = 1; pageNumber <= totalPage; pageNumber++) {
					IPageHint hint = pageHintReader.getPageHint(pageNumber);
					int sectionCount = hint.getSectionCount();
					for (int i = 0; i < sectionCount; i++) {
						PageSection section = hint.getSection(i);

						if (section.startOffset <= offset && offset <= section.endOffset) {
							return pageNumber;
						}
					}
				}
			} else if (version == IPageHintReader.VERSION_2 || version == IPageHintReader.VERSION_3
					|| version == IPageHintReader.VERSION_4 || version == IPageHintReader.VERSION_5
					|| version == IPageHintReader.VERSION_6) {
				long totalPage = pageHintReader.getTotalPage();
				for (long pageNumber = 1; pageNumber <= totalPage; pageNumber++) {
					IPageHint hint = pageHintReader.getPageHint(pageNumber);
					int sectionCount = hint.getSectionCount();
					Fragment fragment = new Fragment(new InstanceIDComparator());
					for (int i = 0; i < sectionCount; i++) {
						PageSection section = hint.getSection(i);
						fragment.addSection(section.starts, section.ends);
					}
					fragment.build();
					if (fragment.inFragment(iid)) {
						return pageNumber;
					}
				}
			}
		} catch (IOException ex) {

		}
		return -1;
	}

	@Override
	public long getInstanceOffset(InstanceID iid) {
		if (!isComplete() || (iid == null)) {
			return -1l;
		}
		loadCoreStreamLazily();

		if (indexReader != null) {
			try {
				long offset = indexReader.getOffsetOfInstance(iid.toUniqueString());
				if (offset == -1) {
					offset = indexReader.getOffsetOfInstance(iid.toString());
				}
				return offset;
			} catch (IOException ex) {
				logger.log(Level.WARNING, "failed to get the offset of instance:" + iid.toUniqueString(), ex);
			}
		}
		return -1L;
	}

	@Override
	public long getBookmarkOffset(String bookmark) {
		if (!isComplete()) {
			return -1;
		}
		if (bookmark == null) {
			return -1l;
		}

		loadCoreStreamLazily();
		if (indexReader != null) {
			try {
				return indexReader.getOffsetOfBookmark(bookmark);
			} catch (IOException ex) {
				logger.log(Level.WARNING, "failed to get the offset of bookmark:" + bookmark, ex);
			}
		}
		return -1L;
	}

	/**
	 * @deprecated
	 */
	@Deprecated
	@Override
	public ClassLoader getClassLoader() {
		if (applicationClassLoader != null) {
			return applicationClassLoader;
		}
		synchronized (this) {
			if (applicationClassLoader == null) {
				applicationClassLoader = new ApplicationClassLoader(engine, getOnPreparedRunnable(),
						engine.getConfig().getAppContext());
			}
		}
		return applicationClassLoader;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.birt.report.engine.api.IReportDocument#isComplete()
	 */
	@Override
	public boolean isComplete() {
		return checkpoint == CHECKPOINT_END;
	}

	@Override
	public ReportDesignHandle getReportDesign() {
		IReportRunnable reportRunnable = getReportRunnable();
		if (reportRunnable != null) {
			return (ReportDesignHandle) reportRunnable.getDesignHandle();
		}
		return null;
	}

	@Override
	public Report getReportIR(ReportDesignHandle designHandle) {
		InputStream stream = null;
		try {
			stream = archive.getStream(DESIGN_IR_STREAM);
			EngineIRReader reader = new EngineIRReader();
			Report reportIR = reader.read(stream);
			reportIR.setVersion(getVersion());
			reader.link(reportIR, designHandle);
			return reportIR;
		} catch (IOException ioex) {
			// an error occurs in reading the engine ir
			logger.log(Level.FINE, "Failed to load the engine IR", ioex);
		} finally {
			if (stream != null) {
				try {
					stream.close();
				} catch (IOException ignored) {
				}
				stream = null;
			}
		}
		return null;
	}

	private InstanceID loadInstanceID(ReportContentReaderV3 reader, long offset) throws IOException {
		IContent content = reader.readContent(offset);
		if (content != null) {
			InstanceID iid = content.getInstanceID();
			DocumentExtension ext = (DocumentExtension) content.getExtension(IContent.DOCUMENT_EXTENSION);
			long parentOffset = ext.getParent();
			if (parentOffset != -1) {
				InstanceID pid = loadInstanceID(reader, parentOffset);
				if (pid != null) {
					return new InstanceID(pid, iid);
				}
			}
			return iid;
		}
		return null;
	}

	@Override
	public InstanceID getBookmarkInstance(String bookmark) {
		if (bookmark == null || bookmark.length() == 0) {
			return null;
		}

		long offset = getBookmarkOffset(bookmark);

		if (offset < 0) {
			return null;
		}

		try {
			RAInputStream is = archive.getStream(CONTENT_STREAM);
			try (is) {
				ReportContentReaderV3 reader = new ReportContentReaderV3(new ReportContent(), is,
						applicationClassLoader);
				try {
					return loadInstanceID(reader, offset);
				} finally {
					reader.close();
				}
			}
		} catch (IOException ioe) {
			logger.log(Level.FINE, "Failed to get the instance ID of the bookmark: " + bookmark, ioe);
		}
		return null;
	}

	private Boolean isReportlet;
	private String reportletBookmark;
	private String reportletInstanceID;

	@Override
	public boolean isReporltetDocument() throws IOException {
		return loadReportletStream();
	}

	@Override
	public String getReportletBookmark() throws IOException {
		if (loadReportletStream()) {
			return reportletBookmark;
		}
		return null;
	}

	@Override
	public InstanceID getReportletInstanceID() throws IOException {
		if (loadReportletStream()) {
			return InstanceID.parse(reportletInstanceID);
		}
		return null;
	}

	private boolean loadReportletStream() throws IOException {
		if (isReportlet == null) {
			if (archive.exists(REPORTLET_DOCUMENT_STREAM)) {
				RAInputStream in = archive.getInputStream(REPORTLET_DOCUMENT_STREAM);
				try (in) {
					int version = in.readInt();
					if (version != REPORTLET_DOCUMENT_VERSION_0) {
						throw new IOException("unsupported reportlet document " + version);
					}
					int size = in.readInt();
					byte[] bytes = new byte[size];
					in.readFully(bytes, 0, size);
					DataInputStream s = new DataInputStream(new ByteArrayInputStream(bytes));
					reportletBookmark = IOUtil.readString(s);
					reportletInstanceID = IOUtil.readString(s);
					isReportlet = Boolean.TRUE;
				}
			} else {
				isReportlet = Boolean.FALSE;
			}
		}
		return isReportlet.booleanValue();
	}

	HashMap<String, IReportDocumentExtension> extensions = new HashMap<>();

	@Override
	synchronized public IReportDocumentExtension getDocumentExtension(String name) throws EngineException {
		IReportDocumentExtension extension = extensions.get(name);
		if (extension == null) {
			IReportEngineExtension engineExtension = this.engine.getEngineExtension(name);
			if (engineExtension != null) {
				extension = engineExtension.createDocumentExtension(this);
				extensions.put(name, extension);
			}
		}
		return extension;
	}

	/**
	 * @deprecated
	 */
	@Deprecated
	synchronized public ITreeNode getTOCTree() throws EngineException {
		if (!isComplete()) {
			return null;
		}
		loadCoreStreamLazily();
		try {
			if (tocReader == null) {
				tocReader = new TOCReader(archive, getClassLoader());
			}
			return tocReader.readTree();
		} catch (IOException ex) {
			throw new EngineException(MessageConstants.FAILED_TO_LOAD_TOC_TREE_EXCEPTION, ex);
		}
	}

	@Override
	public ITOCReader getTOCReader(ClassLoader loader) throws EngineException {
		loadCoreStreamLazily();
		if (cachedTreeV0 != null) {
			// it is safe to reuse the tocReaderV0 as the close is empty.
			return new TOCReaderV0(cachedTreeV0);
		}
		try {
			return new TOCReader(archive, loader);
		} catch (IOException ex) {
			throw new EngineException(MessageConstants.FAILED_TO_LOAD_TOC_TREE_EXCEPTION, ex);
		}
	}

	public void setEngineCacheEntry(LinkedEntry<ReportDocumentReader> entry) {
		this.engineCacheEntry = entry;
	}

	@Override
	public String getSystemId() {
		return systemId;
	}

	@Override
	public List<String> getDocumentErrors() {
		RunStatusReader statusReader = new RunStatusReader(this);
		try {
			return statusReader.getGenerationErrors();
		} finally {
			statusReader.close();
		}
	}
}
